#!/bin/bash
#
# A simple test suite for ccache.
#
# Copyright (C) 2002-2007 Andrew Tridgell
# Copyright (C) 2009-2020 Joel Rosdahl and other contributors
#
# See doc/AUTHORS.adoc for a complete list of contributors.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA


# only use ansi color codes if output is to a terminal
if [[ -t 1 ]]; then
    ansi_boldgreen='\033[1;32m'
    ansi_boldred='\033[1;31m'
    ansi_bold='\033[1m'
    ansi_reset='\033[1;0m'
fi

green() {
    printf "$ansi_boldgreen$*$ansi_reset\n"
}

red() {
    printf "$ansi_boldred$*$ansi_reset\n"
}

bold() {
    printf "$ansi_bold$*$ansi_reset\n"
}

test_failed() {
    echo
    red FAILED
    echo
    echo "Test suite:     $(bold $CURRENT_SUITE)"
    echo "Test case:      $(bold $CURRENT_TEST)"
    echo "Failure reason: $(red "$1")"
    echo
    echo "ccache -s:"
    $CCACHE -s
    echo
    echo "Test data and log file have been left in $TESTDIR / $TEST_FAILED_SYMLINK"
    symlink_testdir_on_failure
    exit 1
}

find_compiler() {
    local name=$1
    perl -e '
        use File::Basename;
        my $cc = $ARGV[0];
        $cc = basename($cc) if readlink($cc) =~ "ccache";
        if ($cc =~ m!^/!) {
            print $cc;
            exit;
        }
        foreach my $dir (split(/:/, $ENV{PATH})) {
            $path = "$dir/$cc";
            if (-x $path && readlink($path) !~ "ccache") {
                print $path;
                exit;
            }
        }' $name
}

generate_code() {
    local nlines=$1
    local outfile=$2

    rm -f $outfile
    for i in $(seq $nlines); do
        echo "int foo_$i(int x) { return x; }" >>$outfile
    done
}

remove_cache() {
    if [ -d $CCACHE_DIR ]; then
        chmod -R +w $CCACHE_DIR
        rm -rf $CCACHE_DIR
    fi
}

clear_cache() {
    $CCACHE -Cz >/dev/null
}

sed_in_place() {
    local expr=$1
    shift

    for file in $*; do
        sed "$expr" $file >$file.sed
        mv $file.sed $file
    done
}

backdate() {
    if [[ $1 =~ ^[0-9]+$ ]]; then
        m=$1
        shift
    else
        m=0
    fi
    touch -t 1999010100$(printf "%02u" $m) "$@"
}

file_size() {
    wc -c $1 | awk '{print $1}'
}

objdump_cmd() {
    if $HOST_OS_APPLE; then
        xcrun dwarfdump -r 0 $1
    elif $HOST_OS_FREEBSD; then
        objdump -W $1
    elif $HOST_OS_WINDOWS || $HOST_OS_CYGWIN; then
        strings $1 # for some reason objdump only shows the basename of the file, so fall back to brute force and ignorance
    else
        objdump -g $1
    fi
}

expect_stat() {
    local stat="$1"
    local expected_value="$2"
    local value="$(echo $($CCACHE -s | fgrep "$stat" | cut -c34-))"

    if [ "$expected_value" != "$value" ]; then
        test_failed "Expected \"$stat\" to be $expected_value, actual $value"
    fi
}

expect_file_exists() {
    if [ ! -f "$1" ]; then
        test_failed "Expected $1 to exist, but it's missing"
    fi
}

expect_file_missing() {
    if [ -f "$1" ]; then
        test_failed "Expected $1 to be missing, but it exists"
    fi
}

expect_equal_files() {
    if [ ! -e "$1" ]; then
        test_failed "expect_equal_files: $1 missing"
    fi
    if [ ! -e "$2" ]; then
        test_failed "expect_equal_files: $2 missing"
    fi
    if ! cmp -s "$1" "$2"; then
        test_failed "$1 and $2 differ"
    fi
}

expect_different_files() {
    if [ ! -e "$1" ]; then
        test_failed "expect_different_files: $1 missing"
    fi
    if [ ! -e "$2" ]; then
        test_failed "expect_different_files: $2 missing"
    fi
    if cmp -s "$1" "$2"; then
        test_failed "$1 and $2 are identical"
    fi
}

is_equal_object_files() {
    if $HOST_OS_LINUX && $COMPILER_TYPE_CLANG; then
        if ! which eu-elfcmp >/dev/null 2>&1; then
            test_failed "Please install elfutils to get eu-elfcmp"
        fi
        eu-elfcmp -q "$1" "$2"
    elif $HOST_OS_FREEBSD && $COMPILER_TYPE_CLANG; then
        elfdump -a -w "$1".dump "$1"
        elfdump -a -w "$2".dump "$2"
        # these were the elfdump fields that seemed to differ (empirically)
        diff -I e_shoff -I sh_size -I st_name "$1".dump "$2".dump > /dev/null
    else
        cmp -s "$1" "$2"
    fi
}

expect_equal_object_files() {
    is_equal_object_files "$1" "$2"
    if [ $? -ne 0 ]; then
        test_failed "Objects differ: $1 != $2"
    fi
}

expect_file_content() {
    local file="$1"
    local content="$2"

    if [ ! -f "$file" ]; then
        test_failed "$file not found"
    fi
    if [ "$(cat $file)" != "$content" ]; then
        test_failed "Bad content of $file.\nExpected: $content\nActual: $(cat $file)"
    fi
}

expect_file_contains() {
    local file="$1"
    local string="$2"

    if [ ! -f "$file" ]; then
        test_failed "$file not found"
    fi
    if ! grep -q "$string" "$file"; then
        test_failed "File $file does not contain: $string. Actual content: $(cat $file)"
    fi
}

expect_file_count() {
    local expected=$1
    local pattern=$2
    local dir=$3
    local actual=`find $dir -type f -name "$pattern" | wc -l`
    if [ $actual -ne $expected ]; then
        test_failed "Found $actual (expected $expected) $pattern files in $dir"
    fi
}

# Verify that $1 is newer than (or same age as) $2.
expect_file_newer_than() {
    local newer_file=$1
    local older_file=$2
    if [ "$newer_file" -ot "$older_file" ]; then
        test_failed "$newer_file is older than $older_file"
    fi
}

run_suite() {
    local suite_name=$1

    CURRENT_SUITE=$suite_name

    cd $ABS_TESTDIR
    rm -rf $ABS_TESTDIR/fixture

    if type SUITE_${suite_name}_PROBE >/dev/null 2>&1; then
        mkdir $ABS_TESTDIR/probe
        cd $ABS_TESTDIR/probe
        local skip_reason="$(SUITE_${suite_name}_PROBE)"
        cd $ABS_TESTDIR
        rm -rf $ABS_TESTDIR/probe
        if [ -n "$skip_reason" ]; then
            echo "Skipped test suite $suite_name [$skip_reason]"
            return
        fi
    fi

    printf "Running test suite %s" "$(bold $suite_name)"
    SUITE_$suite_name
    echo
}

TEST() {
    CURRENT_TEST=$1

    while read name; do
        unset $name
    done <<EOF
$(env | sed -n 's/^\(CCACHE_[A-Z0-9_]*\)=.*$/\1/p')
EOF
    unset GCC_COLORS

    export CCACHE_CONFIGPATH=$ABS_TESTDIR/ccache.conf
    export CCACHE_DETECT_SHEBANG=1
    export CCACHE_DIR=$ABS_TESTDIR/.ccache
    export CCACHE_LOGFILE=$ABS_TESTDIR/ccache.log
    export CCACHE_NODIRECT=1

    # Many tests backdate files, which updates their ctimes. In those tests, we
    # must ignore ctimes. Might as well do so everywhere.
    DEFAULT_SLOPPINESS=include_file_ctime
    export CCACHE_SLOPPINESS="$DEFAULT_SLOPPINESS"

    CCACHE_COMPILE="$CCACHE $COMPILER"

    if $VERBOSE; then
        printf "\n  %s" "$CURRENT_TEST"
    else
        printf .
    fi

    cd /
    remove_cache
    rm -rf $ABS_TESTDIR/run $ABS_TESTDIR/run.real

    # Verify that tests behave well when apparent CWD != actual CWD.
    mkdir $ABS_TESTDIR/run.real
    ln -s run.real $ABS_TESTDIR/run

    cd $ABS_TESTDIR/run
    if type SUITE_${suite_name}_SETUP >/dev/null 2>&1; then
        SUITE_${suite_name}_SETUP
    fi
}

# =============================================================================
# main program

export LC_ALL=C

if pwd | grep '[^A-Za-z0-9/.,=_%+-]' >/dev/null 2>&1; then
    cat <<EOF
Error: The test suite doesn't work in directories with whitespace or other
funny characters in the name. Sorry.
EOF
    exit 1
fi

# Remove common ccache directories on host from PATH variable
HOST_CCACHE_DIRS="/usr/lib/ccache/bin
/usr/lib/ccache"
for HOST_CCACHE_DIR in $HOST_CCACHE_DIRS; do
    PATH="$(echo "$PATH:" | awk -v RS=: -v ORS=: '$0 != "'$HOST_CCACHE_DIR'"' | sed 's/:*$//')"
done
export PATH

if [ -n "$CC" ]; then
    COMPILER="$CC"
else
    COMPILER=gcc
fi
if [ -z "$CCACHE" ]; then
    CCACHE=`pwd`/ccache
fi

COMPILER_TYPE_CLANG=false
COMPILER_TYPE_GCC=false

COMPILER_USES_LLVM=false
COMPILER_USES_MINGW=false

HOST_OS_APPLE=false
HOST_OS_LINUX=false
HOST_OS_FREEBSD=false
HOST_OS_WINDOWS=false
HOST_OS_CYGWIN=false

compiler_version="`$COMPILER --version 2>&1 | head -1`"
case $compiler_version in
    *gcc*|*g++*|2.95*)
        COMPILER_TYPE_GCC=true
        ;;
    *clang*)
        COMPILER_TYPE_CLANG=true
        ;;
    *)
        echo "WARNING: Compiler $COMPILER not supported (version: $compiler_version) -- not running tests" >&2
        exit 0
        ;;
esac

case $compiler_version in
    *llvm*|*LLVM*)
        COMPILER_USES_LLVM=true
        ;;
    *MINGW*|*mingw*)
        COMPILER_USES_MINGW=true
        ;;
esac

case $(uname -s) in
    *MINGW*|*mingw*)
        HOST_OS_WINDOWS=true
        ;;
    *CYGWIN*|*MSYS*)
        HOST_OS_CYGWIN=true
        ;;
    *Darwin*)
        HOST_OS_APPLE=true
        ;;
    *Linux*)
        HOST_OS_LINUX=true
        ;;
    *FreeBSD*)
        HOST_OS_FREEBSD=true
        ;;
esac

if $HOST_OS_WINDOWS; then
    PATH_DELIM=";"
else
    PATH_DELIM=":"
fi

if $HOST_OS_APPLE; then
    SDKROOT=$(xcrun --sdk macosx --show-sdk-path 2>/dev/null)
    if [ "$SDKROOT" = "" ]; then
        echo "Error: xcrun --show-sdk-path failure"
        exit 1
    fi

    SYSROOT="-isysroot `echo \"$SDKROOT\" | sed 's/ /\\ /g'`"
else
    SYSROOT=
fi

# ---------------------------------------

all_suites="
base
nocpp2
cpp1
multi_arch
serialize_diagnostics
sanitize_blacklist
debug_prefix_map
split_dwarf
masquerading
hardlink
fileclone
direct
direct_gcc
depend
basedir
no_compression
readonly
readonly_direct
cleanup
pch
modules
upgrade
input_charset
nvcc
nvcc_direct
nvcc_ldir
nvcc_nocpp2
"

for suite in $all_suites; do
    . $(dirname $0)/suites/$suite.bash
done

# ---------------------------------------

TESTDIR=testdir.$$
TEST_FAILED_SYMLINK=testdir.failed
ABS_TESTDIR=$PWD/$TESTDIR
rm -rf $TESTDIR
mkdir $TESTDIR

START_PWD="$PWD"
symlink_testdir_on_failure() {
    cd "$START_PWD"
    rm -f "$TEST_FAILED_SYMLINK"
    ln -s "$TESTDIR" "$TEST_FAILED_SYMLINK"
}

cd $TESTDIR || exit 1

compiler_bin=$(echo $COMPILER | awk '{print $1}')
compiler_args=$(echo $COMPILER | awk '{$1 = ""; print}')
REAL_COMPILER_BIN=$(find_compiler $compiler_bin)
REAL_COMPILER="$REAL_COMPILER_BIN$compiler_args"

if [ "$REAL_COMPILER" = "$COMPILER" ]; then
    echo "Compiler:         $COMPILER"
else
    echo "Compiler:         $COMPILER ($REAL_COMPILER)"
fi
echo "Compiler version: $($COMPILER --version | head -n 1)"

REAL_NVCC=$(find_compiler nvcc)
REAL_CUOBJDUMP=$(find_compiler cuobjdump)
if [ -n "$REAL_NVCC" ]; then
    echo "CUDA compiler:    $($REAL_NVCC --version | tail -n 1) ($REAL_NVCC)"
else
    echo "CUDA compiler:    not available"
fi
echo

VERBOSE=false
[ "$1" = "-v" ] && { VERBOSE=true; shift; }

suites="$*"
if [ -z "$suites" ]; then
    suites="$all_suites"
fi

for suite in $suites; do
    run_suite $suite
done

cd /
rm -rf $ABS_TESTDIR
green PASSED
exit 0
